/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace DaveSexton.DocProject.Engine
{
	public delegate void ExecuteStepCallback(IBuildStep step, BuildContext context);

	public sealed class BuildStepExecutionEngine : System.ComponentModel.Component
	{
		#region Private / Protected
		private enum StepResult
		{
			SuccessContinue,
			SuccessBreak,
			ErrorBreak,
			ErrorContinue,
			SkippedBreak,
			SkippedContinue
		}

		private readonly BuildContext context;
		private readonly ExecuteStepCallback callback;

		private delegate StepResult BuildAsync();
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="BuildStepExecutionEngine" /> class.
		/// </summary>
		public BuildStepExecutionEngine(BuildContext context, ExecuteStepCallback callback)
		{
			if (context == null)
				throw new ArgumentNullException("context");

			if (callback == null)
				throw new ArgumentNullException("callback");

			this.callback = callback;
			this.context = context;
		}
		#endregion

		#region Methods
		[System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.LinkDemand,
			Flags = System.Security.Permissions.SecurityPermissionFlag.Infrastructure)]
		public override object InitializeLifetimeService()
		{
			return null;
		}

		public ICollection<Exception> ExecuteAll()
		{
			List<Exception> exceptions = new List<Exception>(8);

			for (int i = 0; i < context.Steps.Count; i++)
			{
				IBuildStep step = context.Steps[i];

				if (context.BuildType == BuildType.Partial && !step.IncludeInPartialBuild)
					continue;

				// allow user to click cancel
				Application.DoEvents();

				if (context.Canceled)
					break;

				Exception ex;
				StepResult result = ExecuteStep(step, i + 1, false, out ex);

				switch (result)
				{
					case StepResult.ErrorBreak:
						exceptions.Add(ex);
						context.AddFailedStep(step);
						goto Return;
					case StepResult.ErrorContinue:
						exceptions.Add(ex);
						context.AddFailedStep(step);
						break;
					case StepResult.SuccessContinue:
						context.AddSuccessfulStep(step);
						break;
					case StepResult.SuccessBreak:
						context.AddSuccessfulStep(step);
						goto Return;
					case StepResult.SkippedBreak:
						context.SkippedStep(step);
						goto Return;
					case StepResult.SkippedContinue:
						context.SkippedStep(step);

						context.TraceLine();
						context.TraceLine(Resources.Text.SkippedStep);
						break;
				}

				if (context.Canceled)
					break;
			}

		Return:
			return exceptions.AsReadOnly();
		}

		public bool ExecuteSequence(params IBuildStep[] steps)
		{
			if (steps == null)
				throw new ArgumentNullException("steps");

			foreach (IBuildStep step in steps)
			{
				if (step == null)
					continue;

				// allow user to click cancel
				Application.DoEvents();

				if (context.Canceled)
					return false;

				Exception ex;
				StepResult result = ExecuteStep(step, -1, true, out ex);

				switch (result)
				{
					case StepResult.ErrorContinue:
					case StepResult.SuccessContinue:
						break;
					case StepResult.ErrorBreak:
						throw ex;
					case StepResult.SuccessBreak:
					case StepResult.SkippedBreak:
						return false;
					case StepResult.SkippedContinue:
						context.TraceLine();
						context.TraceLine(Resources.Text.SkippedStep);
						break;
				}

				if (context.Canceled)
					return false;
			}

			return true;
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private StepResult ExecuteStep(IBuildStep step, int number, bool outOfBand, out Exception exception)
		{
			exception = null;
			
			try
			{
				if (!outOfBand)
				{
					context.TraceLine();
					context.TraceLine("Step {0} of {1}: {2}", number, context.Steps.Count, step.Name);
				}

				if (step.RunInBackground)
					return ExecuteStepAsync(step, outOfBand);
				else
					return ExecuteStepInternal(step, false, outOfBand);
			}
			catch (Exception ex)
			{
				exception = ex;

				context.TraceLine();

				string stepInfo;

				if (outOfBand)
				{
					stepInfo = string.Format(System.Globalization.CultureInfo.CurrentCulture,
						"{0} {1}", step.Name, ex.GetType().FullName);

					context.TraceLine(stepInfo + ": " + ex.Message);
				}
				else
				{
					stepInfo = string.Format(System.Globalization.CultureInfo.CurrentCulture,
						"Step {0} {1}: {2}:", number, ex.GetType().FullName, step.Name);

					context.TraceLine(stepInfo);
					context.TraceLine(ex.Message);
				}

				context.AddErrorToList(stepInfo, ex.Message);

				Log.Exception(ex, stepInfo);

				return (step.ContinueOnError) ? StepResult.ErrorContinue : StepResult.ErrorBreak;
			}
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private StepResult ExecuteStepAsync(IBuildStep step, bool outOfBand)
		{
			StepResult result = StepResult.SkippedBreak;	// default value set to SkippedBreak if the async process is canceled
			EventWaitHandle waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
			Exception asyncError = null;

			while (!ThreadPool.QueueUserWorkItem((WaitCallback) delegate(object state)
				{
					try
					{
						result = ExecuteStepInternal(step, true, outOfBand);
					}
					catch (Exception ex)
					{
						asyncError = ex;
					}
					finally
					{
						waitHandle.Set();
					}
				}, null))
			{
				// allow user to click cancel
				Application.DoEvents();

				if (context.Canceled)
					return StepResult.SkippedBreak;
			}

			while (!waitHandle.WaitOne(0, false) && !context.Canceled)
				// allow user to click cancel
				Application.DoEvents();

			if (context.Canceled)
			{
				context.TraceLine(Resources.Text.CancelingBackgroundStep);

				step.Cancel();

				while (!waitHandle.WaitOne(200, false))
				// Periodically waiting for the waitHandle to be set fixes a bug whereby an async step
				// would finish and update the status text and progress bar after they were already
				// reset by the build engine, which completed on the UI thread before the async step.
				{
					// Avoid deadlocks by allowing the async thread to send its console messages
					// and perform its UI-synchronized tasks (probably using the BuildContext.Sync property).
					Application.DoEvents();
				}

				context.TraceLine(Resources.Text.BackgroundStepCanceled);
			}
			
			if (asyncError != null)
				throw asyncError;

			return result;
		}

		private StepResult ExecuteStepInternal(IBuildStep step, bool isAsync, bool outOfBand)
		{
			if (!outOfBand)
			{
				if (context.processComponent != null)
				{
					try
					{
						if (!context.processComponent.BeforeExecuteStep(step, context))
							return StepResult.SkippedContinue;
					}
					catch (Exception ex)
					{
						throw new BuildProcessComponentException(context.processComponent, ex);
					}
				}

				if (!isAsync)
					// allow user to click cancel
					Application.DoEvents();

				if (context.Canceled)
					return StepResult.SkippedBreak;
			}

			callback(step, context);

			if (!isAsync)
				// allow user to click cancel
				Application.DoEvents();

			if (!outOfBand)
			{
				if (context.processComponent != null)
				{
					try
					{
						context.processComponent.AfterExecuteStep(step, context);
					}
					catch (Exception ex)
					{
						throw new BuildProcessComponentException(context.processComponent, ex);
					}
				}
			}
			
			return (context.Canceled) ? StepResult.SuccessBreak : StepResult.SuccessContinue;
		}
		#endregion
	}
}
